function fireHandler(changeHandler) {
    if (changeHandler.timeout) {
        clearTimeout(changeHandler.timeout);
    }
    changeHandler.timeout = setTimeout(changeHandler, 1000);
}

function initDataTable($scope) {

    var $downloadsTable = $('#downloads_table');

    var dt = $downloadsTable.DataTable({
        "dom": "<'H'f><'#editor_console'>t<'F'pi<'#selection_controls'>>",
        "jQueryUI": true,
        "scrollY": 480,
        "scrollCollapse": true,
        "pageLength": 100,
        "paging": false,
        "columns": [
            {
                "class": "details-control",
                "orderable": false,
                "data": "",
                "defaultContent": ""
            },
            {"title": "Name", data: "name", "width": "20%"},
            {"title": "Url", data: "url", "width": "80%"}
        ],
        "order": [[1, 'asc']],
        "search": {
            "search": $scope.search
        },
        "autoWidth": false,
        "createdRow": function (row, data, dataIndex) {
            var $row = $(row);
            $row.attr("id", "row_" + dataIndex);
            $row.on('click', ':not(td.details-control)', function (event) {
                var $tr = $(this).closest('tr');
                $tr.toggleClass('ui-selected');
                if (!(event.metaKey || event.ctrlKey)) {
                    $tr.siblings().removeClass('ui-selected');
                }
            });
            $row.on('click', 'td.details-control', function (evt) {
                var $tr = $(this).closest('tr');
                var dtRow = dt.row($tr);
                var data = dtRow.data();
                if (data.isOpen) {
                    $tr.removeClass('open');
                    dtRow.child.hide();
                    data.isOpen = false;
                } else {
                    $tr.addClass('open');
                    var $template = $('<download-info></download-info>');
                    $template.prop('download', data);
                    dtRow.child($template).show();
                    data.isOpen = true;
                }
            });
        }
    });

    $downloadsTable.closest('div').selectableScroll({distance: 10, filter: "tr"});

    dt.on('draw', function () {

        $('#downloads_table').find('tbody').find('tr').each(function (dataIndex, tr) {

            var $row = $(tr);

            /*
             * Add tooltips
             */
            $row.tooltip({
                items: "td:nth-child(2), th:nth-child(2)",
                content: function () {
                    return this.textContent;
                }
            });

            var data = dt.row($row).data();
            if (data && data.isOpen) {
                data.isOpen = false;
                $row.children('td.details-control').trigger('click');
            }

            var $downloadItem = $('<download-item></download-item>');
            $row.children('td.details-control').append($downloadItem);
        });
    });

    var $downloadsTableFilter = $("#downloads_table_filter");

    var $search = $downloadsTableFilter.find('input');
    dt.on('search.dt', function () {
        $scope.search = $search.val();
    });
    $downloadsTableFilter.append($search);

    dt.consoleEditor = false;

    dt.openEditor = function (editorPaneId, editorMode) {
        try {
            var modes = {
                "mode/javascript": {
                    "text": '$.map($(document).find("' + $scope.fillSelector() + '"), $.targets)',
                    "delimiter": '.',
                    "action": "execJavascript"
                },
                "mode/xquery": {
                    "text": './/' + $scope.fillSelector(),
                    "delimiter": '/',
                    "action": "applyXQuery"
                },
                "mode/custom": {
                    "text": "/* This is just an idea not implemented yet! */",
                    "delimiter": '?',
                    "action": ""
                }
            };

            var editor = ace.edit(editorPaneId);
            editor.setTheme("ace/theme/chrome");

            var options = modes[editorMode];
            editor.setValue(options.text);

            var session = editor.getSession();
            session.setMode(editorMode);
            session.on('change', function (e) {
                var data = e.data;
                if (data.action == "insertText" && data.text == options.delimiter) {
                    var action = options.action;
                    var end = data.range.start;
                    var selector = editor.session.getTextRange({start: {column: 0, row: 0}, end: {column: end.column, row: end.row}});
                    $scope.sendMessage(action, selector);
                }
            });

            $scope.sendMessage(options.action, editor.getValue());

            return dt.consoleEditor = editor;

        } catch (e) {
            alert('cannot open ACE editor\n\n');
            console.error("error opening editor", e.stack);
        }
    };

    dt.closeEditor = function () {
        dt.consoleEditor.destroy();
        dt.consoleEditor = false;
    };

    $('<div id="editor_pane"></div>').appendTo('#editor_console');

    $("#editor_mode").buttonset();
    $("input[name='editor_mode']").on("change", function (event) {
        dt.closeEditor();
        dt.openEditor("editor_pane", event.target.id);
    });

    $('#editor_button').button({icons: {primary: "ui-icon-pencil"}, text: false}).on('click', function (event) {
        $('#editor_console').slideToggle("fast", function () {
            if (!dt.consoleEditor) {
                var mode = $('#editor_mode').find('label.ui-state-active').attr('for');
                dt.openEditor("editor_pane", mode);
            } else {
                $('#editor_button').button("option", {"icons": {"primary": "ui-icon-pencil"}});
                dt.closeEditor();
            }

            $('#editor_button').button("option", {"icons": {"primary": dt.consoleEditor ? "ui-icon-close" : "ui-icon-pencil"}});
        });
        $('#advanced_selectors').toggle("fast");
        $('#simple_selectors').toggle("fast");
    });

    $('#crawl_button').button({icons: {primary: "ui-icon-arrowthickstop-1-s"}, text: false}).on('click', function (event) {
        $scope.sendMessage('crawl', $scope.fillSelector());
    });

    var $selectionControls = $("#selection_controls");
    $('<button id="select_all">All</button>').appendTo($selectionControls).button({icons: {primary: "ui-icon-circle-check"}}).on('click', function (event) {
        $(dt.$('tr', {"filter": "applied"})).addClass('ui-selected');
        $("#select_none").focus();
    });
    $('<button id="select_none">None</button>').appendTo($selectionControls).button({icons: {primary: "ui-icon-circle-close"}}).on('click', function (event) {
        dt.rows().nodes().to$().removeClass('ui-selected');
        $("#select_all").focus();
    });
    $('<button id="download" class="green">Download</button>').appendTo($selectionControls).button({icons: {primary: "ui-icon-circle-arrow-s"}}).on('click', function (event) {

        var $download = $('#download');

        if ($download.prop('running')) {
            console.log("stopping active downloads...");

            $download.prop('running', false);

            $.each(dt.rows('.ui-selected').data(), function (index, item) {
                console.log($(item));
            });

            $download.button("option", "label", "Download");
            $download.button("option", "icons", {primary: "ui-icon-circle-arrow-s"});

        } else {
            console.log(dt.rows('.ui-selected').data().length + ' row(s) selected');
            console.log("starting downloads...");

            $download.prop('running', true);
            $download.prop('index', 0);

            var queued = 0;
            var activeDownloads = new Set();

            function resumeQueuing() {

                var index = $download.prop('index');
                console.log("resuming downloads queueing at index: %d", index);

                var data;
                var row;

                while ($download.prop('running') && (row = dt.row(index)).data()) {

                    var $node = $(row.node());

                    if ($node.hasClass('ui-selected')) {

                        console.log("row at index: %d is selected, trying to download it...", index);

                        if (queued < 3) {
                            var url = data.url;
                            console.log("queued...");
                            queued++;
                            $node.removeClass('ui-selected');
                            chrome.downloads.download({filename: data.name, url: data.url}, function (downloadId) {
                                data.downloadId = downloadId;
                                console.log("downloading: %d - %s", downloadId, url);
                                activeDownloads.add(downloadId);
                            });
                        } else {
                            console.log("queue is full, returning for now...");
                            return;
                        }
                    }

                    $download.prop('index', ++index);
                }

                if (queued == 0) {
                    console.log("downloads list processed");
                    chrome.downloads.onChanged.removeListener(completionListener);
                    console.log("removed completion listener");
                    $download.prop('running', false);
                    $download.button("option", "label", "Download");
                    $download.button("option", "icons", {primary: "ui-icon-circle-arrow-s"});
                }
            }

            function completionListener(downloadDelta) {
                var downloadId = downloadDelta.id;
                console.log("id: " + id + " from: " + downloadDelta.state.previous + " to: " + downloadDelta.state.current);
                if (downloadDelta.state.current == "complete" && activeDownloads.has(downloadId)) {
                    activeDownloads.delete(downloadId);
                    --queued;
                    console.log("there are now %d queued downloads", queued);
                    resumeQueuing();
                }
            }

            if (chrome.downloads) {
                chrome.downloads.onChanged.addListener(completionListener);
                console.log("registered completion listener");

                resumeDownload();
                console.log("start queueing...");
            }

            $download.button("option", "label", "Stop");
            $download.button("option", "icons", {primary: "ui-icon-cancel"});
        }

        $(this).focus();
    });
    $('<button id="erase" class="red">Erase</button>').appendTo($selectionControls).button({icons: {primary: "ui-icon-circle-close"}, text: false}).on('click', function (event) {
        dt.rows('.ui-selected').remove().draw();
    });

    $(document).ready(function () {

        $("<div id='folder_dialog' title='Load/Save Slots'><p>Choose a slot</p></div>").dialog({
            autoOpen: false,
            resizable: false,
            modal: true,
            width: 'auto',
            close: function (event, ui) {

            }
        });

        $('#folder_button').button({icons: {primary: "ui-icon-folder-collapsed"}, text: false}).on('click', function (event) {

            $("#folder_dialog").empty().dialog("open");

            var downloadsLists = localStorage['downloads_lists'];
            if (downloadsLists) try {
                downloadsLists = JSON.parse(downloadsLists);
                $.each(downloadsLists, function (i, e) {
                    var $line = $('<div/>').appendTo('#folder_dialog');
                    $('<div class="folder_dialog_item"><button class="ui-widget load green">Load</button><input type="text" value="' + i + '" class="ui-widget" readonly="readonly"><button class="ui-widget delete red">Delete</button></div>').appendTo($line);
                });
            } catch (e) {
                console.log("unable to parse localStorage['downloads_lists']", e);
                downloadsLists = {};
            }
            var $line = $('<div/>').appendTo('#folder_dialog');
            $('<div class="folder_dialog_item"><button id="save_new" class="ui-widget save green">Save</button><input type="text" value="New Save..." class="ui-widget"></div>').appendTo($line);

            $('#folder_dialog').find('button.save').button({icons: {primary: "ui-icon-disk"}, text: false}).on('click', function (event) {
                var name = $(event.target).closest('div').children('input').val();
                downloadsLists[name] = [];
                $.each($('#downloads_table').DataTable().rows().data(), function (i, e) {
                    downloadsLists[name].push({
                        id: e.id,
                        name: e.name,
                        url: e.url,
                    });
                });
                localStorage['downloads_lists'] = JSON.stringify(downloadsLists);
                console.log('saved: %s', name);
                $("#folder_dialog").dialog("close");
            });
            $('#folder_dialog').find('button.load').button({icons: {primary: "ui-icon-folder-open"}, text: false}).on('click', function (event) {
                var name = $(event.target).closest('div').children('input').val();
                dt.clear();
                dt.loadContent(downloadsLists[name]);
                console.log('loaded: %s', name);
                $("#folder_dialog").dialog("close");
            });
            $('#folder_dialog').find('button.delete').button({icons: {primary: "ui-icon-trash"}, text: false}).on('click', function (event) {
                var name = $(event.target).closest('div').children('input').val();
                delete downloadsLists[name];
                localStorage['downloads_lists'] = JSON.stringify(downloadsLists);
                console.log('deleted: %s', name);
                $("#folder_dialog").dialog("close");
            });
        });

        var $downloadsSelector = $("#selectors_pane");
        $downloadsSelector
            .insertBefore($downloadsTableFilter)
            .show();
        $downloadsSelector
            .find('select')
            .selectmenu({
                select: function (event, ui) {
                    var modelName = $(this).attr('ng-model');
                    var uiItemValue = $scope[modelName + "_option"] = parseInt(ui.item.value);
                    $scope[modelName] = $scope[modelName + 's'][uiItemValue];
                    fireHandler($scope.selectorChange);
                }
            });
    });

    dt.loadContent = function (dataSet) {
        dt.rows.add(dataSet);
        dt.draw();
    };

    return dt;
}

function extractFilename(url) {
    return url.substring(url.lastIndexOf('/') + 1).replace(/_/g, ' ');
}

function translateDataSet(baseUrl, dataSet) {

    baseUrl = baseUrl.substr(0, baseUrl.lastIndexOf('/'));

    $.each(dataSet, function (index, item) {
        item.name = extractFilename(decodeURI(item.url));
        if (!/^http/.test(item.url)) {
            item.url = baseUrl + "/" + item.url;
        }
    });
    return dataSet;
}

angular.module('Popup', ['Storage']).controller('MainController', function ($scope, storage) {

    $scope.action = "applyJQuery";
    $scope.targets = storage['targets'];
    $scope.target_option = storage['target_option'];
    $scope.target = $scope.targets[$scope.target_option];
    $scope.patterns = storage['patterns'];
    $scope.pattern = storage['pattern'];
    $scope.selectors = storage['selectors'];
    $scope.selector_option = storage['selector_option'];
    $scope.selector = $scope.selectors[$scope.selector_option];
    $scope.search = storage['search'];

    function persist() {
        storage['targets'] = $scope.targets;
        storage['target_option'] = $scope.target_option;
        storage['selectors'] = $scope.selectors;
        storage['selector_option'] = $scope.selector_option;
        storage['patterns'] = $scope.patterns;
        storage['pattern'] = $scope.pattern;
        storage['search'] = $scope.search;

        console.log("persisting storage data...");
        storage.persist();
    }

    $scope.fillSelector = function () {
        var context = $.extend({pattern: $scope.pattern}, $scope.target);
        return $scope.selector.jquery.fillTemplate(context);
    };

    $scope.selectorChange = function () {
        $scope.sendMessage($scope.action, $scope.fillSelector());
    };

    var dataTable = initDataTable($scope);

    function clearDataTable() {
        dataTable.clear().draw();
    }

    function showLoadingInfo() {
        $('#downloads_table_info').hide().before($('#loading_template').html());
        loadingOn();
    }

    function hideLoadingInfo() {
        loadingOff();
        $('#loading_status').remove();
        $('#downloads_table_info').show();
    }

    function setloadingInfo(text) {
        $('#loading_info').text(text);
    }

    function loadingOn() {
        $('.loading .spinner').show();
    }

    function loadingOff() {
        $('.loading .spinner').hide();
    }


    $scope.sendMessage = function (action, selector) {

        clearDataTable();

        if ("execJavascript" == action) chrome.tabs.executeScript(theTab.id, {code: selector}, function (result) {
            console.log("processing downloadables...");
            dataTable.loadContent(translateDataSet(theTab.url, result[0]));
            hideLoadingInfo();
        });
        else chrome.tabs.query({active: true}, function (tabs) {
            var theTab = tabs[0];
            $scope.title = theTab.title;
            $scope.url = theTab.url;

            chrome.tabs.sendMessage(theTab.id, {
                action: action,
                selector: selector,
                target: $scope.target.attribute
            }, function (response) {
                if (!response) {
                    console.log("no response from content");
                    persist = function () {
                        console.log("illegal state", storage);
                    }
                } else if (response.action == 'processDownloadables') {
                    console.log("processing downloadables...");
                    dataTable.loadContent(translateDataSet(theTab.url, response.dataSet));
                }
                hideLoadingInfo();
            });
        });

        showLoadingInfo();
    };


    if (chrome.tabs) {

        addEventListener("unload", function (event) {
            persist();
        });

        chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
            if ("updateInfo" == request.action) {
                setloadingInfo(request.text);
            }
        });

    } else {

        $('body').addClass('popup');

        $(window).bind("beforeunload", function (event) {
            persist();
        });

        $scope.sendMessage = function (action, selector) {

            clearDataTable();

            setTimeout(function () {
                dataTable.loadContent(translateDataSet("", [
                    {
                        id: 1,
                        url: "http://www.retro64games.net/C64files/c64games/A/A_Day_In_the_Life_Of_A_Prehistoric_Man/A_Day_In_the_Life_Of_A_Prehistoric_Man.zip"
                    },
                    {id: 2, url: "http://www." + selector + ".net/C64files/c64games/A/A_Night_on_the_Town/A_Night_on_the_Town.zip"},
                    {id: 3, url: "http://www." + selector + ".net/C64files/c64games/A/A_Nightmare_on_Elm_Street/A_Nightmare_on_Elm_Street.zip"},
                    {id: 4, url: "http://www." + selector + ".net/C64files/c64games/A/A_Question_Of_Scruples/A_Question_Of_Scruples.zip"},
                    {id: 5, url: "http://www." + selector + ".net/C64files/c64games/A/A_Question_of_Sport/A_Question_of_Sport.zip"},
                    {id: 6, url: "http://www." + selector + ".net/C64files/c64games/A/A_to_Snad_NE/A_to_Snad_NE.zip"},
                    {id: 7, url: "http://www." + selector + ".net/C64files/c64games/A/A_View_to_a_Kill/A_View_to_a_Kill.zip"},
                    {id: 8, url: "http://www." + selector + ".net/C64files/c64games/A/Aaargh!/Aaargh!.zip"},
                    {id: 9, url: "http://www." + selector + ".net/C64files/c64games/A/ABC/ABC.zip"},
                    {id: 0, url: "http://www." + selector + ".net/C64files/c64games/A/Abnormal_Faction/Abnormal_Faction.zip"},
                    {
                        id: 11,
                        url: "http://www.retro64games.net/C64files/c64games/A/A_Day_In_the_Life_Of_A_Prehistoric_Man/A_Day_In_the_Life_Of_A_Prehistoric_Man.zip"
                    },
                    {id: 12, url: "http://www.retro64games.net/C64files/c64games/A/A_Night_on_the_Town/A_Night_on_the_Town.zip"},
                    {id: 13, url: "http://www.retro64games.net/C64files/c64games/A/A_Nightmare_on_Elm_Street/A_Nightmare_on_Elm_Street.zip"},
                    {id: 14, url: "http://www.retro64games.net/C64files/c64games/A/A_Question_Of_Scruples/A_Question_Of_Scruples.zip"},
                    {id: 15, url: "http://www.retro64games.net/C64files/c64games/A/A_Question_of_Sport/A_Question_of_Sport.zip"},
                    {id: 16, url: "http://www.retro64games.net/C64files/c64games/A/A_to_Snad_NE/A_to_Snad_NE.zip"},
                    {id: 17, url: "http://www.retro64games.net/C64files/c64games/A/A_View_to_a_Kill/A_View_to_a_Kill.zip"},
                    {id: 18, url: "http://www.retro64games.net/C64files/c64games/A/Aaargh!/Aaargh!.zip"},
                    {id: 19, url: "http://www.retro64games.net/C64files/c64games/A/ABC/ABC.zip"},
                    {id: 10, url: "http://www.retro64games.net/C64files/c64games/A/Abnormal_Faction/Abnormal_Faction.zip"},
                    {
                        id: 21,
                        url: "http://www.retro64games.net/C64files/c64games/A/A_Day_In_the_Life_Of_A_Prehistoric_Man/A_Day_In_the_Life_Of_A_Prehistoric_Man.zip"
                    },
                    {id: 22, url: "http://www.retro64games.net/C64files/c64games/A/A_Night_on_the_Town/A_Night_on_the_Town.zip"},
                    {id: 23, url: "http://www.retro64games.net/C64files/c64games/A/A_Nightmare_on_Elm_Street/A_Nightmare_on_Elm_Street.zip"},
                    {id: 24, url: "http://www.retro64games.net/C64files/c64games/A/A_Question_Of_Scruples/A_Question_Of_Scruples.zip"},
                    {id: 25, url: "http://www.retro64games.net/C64files/c64games/A/A_Question_of_Sport/A_Question_of_Sport.zip"},
                    {id: 26, url: "http://www.retro64games.net/C64files/c64games/A/A_to_Snad_NE/A_to_Snad_NE.zip"},
                    {id: 27, url: "http://www.retro64games.net/C64files/c64games/A/A_View_to_a_Kill/A_View_to_a_Kill.zip"},
                    {id: 28, url: "http://www.retro64games.net/C64files/c64games/A/Aaargh!/Aaargh!.zip"},
                    {id: 29, url: "http://www.retro64games.net/C64files/c64games/A/ABC/ABC.zip"},
                    {id: 20, url: "http://www.retro64games.net/C64files/c64games/A/Abnormal_Faction/Abnormal_Faction.zip"},
                    {
                        id: 31,
                        url: "http://www.retro64games.net/C64files/c64games/A/A_Day_In_the_Life_Of_A_Prehistoric_Man/A_Day_In_the_Life_Of_A_Prehistoric_Man.zip"
                    },
                    {id: 32, url: "http://www.retro64games.net/C64files/c64games/A/A_Night_on_the_Town/A_Night_on_the_Town.zip"},
                    {id: 33, url: "http://www.retro64games.net/C64files/c64games/A/A_Nightmare_on_Elm_Street/A_Nightmare_on_Elm_Street.zip"},
                    {id: 34, url: "http://www.retro64games.net/C64files/c64games/A/A_Question_Of_Scruples/A_Question_Of_Scruples.zip"},
                    {id: 35, url: "http://www.retro64games.net/C64files/c64games/A/A_Question_of_Sport/A_Question_of_Sport.zip"},
                    {id: 36, url: "http://www.retro64games.net/C64files/c64games/A/A_to_Snad_NE/A_to_Snad_NE.zip"},
                    {id: 37, url: "http://www.retro64games.net/C64files/c64games/A/A_View_to_a_Kill/A_View_to_a_Kill.zip"},
                    {id: 38, url: "http://www.retro64games.net/C64files/c64games/A/Aaargh!/Aaargh!.zip"},
                    {id: 39, url: "http://www.retro64games.net/C64files/c64games/A/ABC/ABC.zip"},
                    {id: 30, url: "http://www.retro64games.net/C64files/c64games/A/Abnormal_Faction/Abnormal_Faction.zip"}
                ]));

                hideLoadingInfo();
            }, 2000);

            showLoadingInfo();
        };

    }

    $scope.selectorChange();

});

